/**
* 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.assertArrayEquals;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertNotNull;
import static org.junit.Assert.assertTrue;
import static org.mockito.Matchers.any;
import static org.mockito.Matchers.anyInt;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.never;
import static org.mockito.Mockito.verify;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collection;
import java.util.List;
import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.ConcurrentLinkedQueue;
import java.util.concurrent.CountDownLatch;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.atomic.AtomicInteger;
import java.util.concurrent.atomic.AtomicReference;
import org.junit.Before;
import org.junit.Test;
import org.mockito.Matchers;
import org.mockito.MockitoAnnotations;
import rx.Notification;
import rx.Observable;
import rx.Observable.OnSubscribe;
import rx.Observer;
import rx.Subscriber;
import rx.Subscription;
import rx.exceptions.TestException;
import rx.functions.Action0;
import rx.functions.Action1;
import rx.functions.Func1;
import rx.internal.util.UtilityFunctions;
import rx.observables.GroupedObservable;
import rx.observers.TestSubscriber;
import rx.schedulers.Schedulers;
public class OperatorGroupByTest {
final Func1<String, Integer> length = new Func1<String, Integer>() {
@Override
public Integer call(String s) {
return s.length();
}
};
@Test
public void testGroupBy() {
Observable<String> source = Observable.just("one", "two", "three", "four", "five", "six");
Observable<GroupedObservable<Integer, String>> grouped = source.lift(new OperatorGroupBy<String, Integer, String>(length));
Map<Integer, Collection<String>> map = toMap(grouped);
assertEquals(3, map.size());
assertArrayEquals(Arrays.asList("one", "two", "six").toArray(), map.get(3).toArray());
assertArrayEquals(Arrays.asList("four", "five").toArray(), map.get(4).toArray());
assertArrayEquals(Arrays.asList("three").toArray(), map.get(5).toArray());
}
@Test
public void testGroupByWithElementSelector() {
Observable<String> source = Observable.just("one", "two", "three", "four", "five", "six");
Observable<GroupedObservable<Integer, Integer>> grouped = source.lift(new OperatorGroupBy<String, Integer, Integer>(length, length));
Map<Integer, Collection<Integer>> map = toMap(grouped);
assertEquals(3, map.size());
assertArrayEquals(Arrays.asList(3, 3, 3).toArray(), map.get(3).toArray());
assertArrayEquals(Arrays.asList(4, 4).toArray(), map.get(4).toArray());
assertArrayEquals(Arrays.asList(5).toArray(), map.get(5).toArray());
}
@Test
public void testGroupByWithElementSelector2() {
Observable<String> source = Observable.just("one", "two", "three", "four", "five", "six");
Observable<GroupedObservable<Integer, Integer>> grouped = source.groupBy(length, length);
Map<Integer, Collection<Integer>> map = toMap(grouped);
assertEquals(3, map.size());
assertArrayEquals(Arrays.asList(3, 3, 3).toArray(), map.get(3).toArray());
assertArrayEquals(Arrays.asList(4, 4).toArray(), map.get(4).toArray());
assertArrayEquals(Arrays.asList(5).toArray(), map.get(5).toArray());
}
@Test
public void testEmpty() {
Observable<String> source = Observable.empty();
Observable<GroupedObservable<Integer, String>> grouped = source.lift(new OperatorGroupBy<String, Integer, String>(length));
Map<Integer, Collection<String>> map = toMap(grouped);
assertTrue(map.isEmpty());
}
@Test
public void testError() {
Observable<String> sourceStrings = Observable.just("one", "two", "three", "four", "five", "six");
Observable<String> errorSource = Observable.error(new RuntimeException("forced failure"));
Observable<String> source = Observable.concat(sourceStrings, errorSource);
Observable<GroupedObservable<Integer, String>> grouped = source.lift(new OperatorGroupBy<String, Integer, String>(length));
final AtomicInteger groupCounter = new AtomicInteger();
final AtomicInteger eventCounter = new AtomicInteger();
final AtomicReference<Throwable> error = new AtomicReference<Throwable>();
grouped.flatMap(new Func1<GroupedObservable<Integer, String>, Observable<String>>() {
@Override
public Observable<String> call(final GroupedObservable<Integer, String> o) {
groupCounter.incrementAndGet();
return o.map(new Func1<String, String>() {
@Override
public String call(String v) {
return "Event => key: " + o.getKey() + " value: " + v;
}
});
}
}).subscribe(new Subscriber<String>() {
@Override
public void onCompleted() {
}
@Override
public void onError(Throwable e) {
e.printStackTrace();
error.set(e);
}
@Override
public void onNext(String v) {
eventCounter.incrementAndGet();
System.out.println(v);
}
});
assertEquals(3, groupCounter.get());
assertEquals(6, eventCounter.get());
assertNotNull(error.get());
}
private static <K, V> Map<K, Collection<V>> toMap(Observable<GroupedObservable<K, V>> observable) {
final ConcurrentHashMap<K, Collection<V>> result = new ConcurrentHashMap<K, Collection<V>>();
observable.toBlocking().forEach(new Action1<GroupedObservable<K, V>>() {
@Override
public void call(final GroupedObservable<K, V> o) {
result.put(o.getKey(), new ConcurrentLinkedQueue<V>());
o.subscribe(new Action1<V>() {
@Override
public void call(V v) {
result.get(o.getKey()).add(v);
}
});
}
});
return result;
}
/**
* Assert that only a single subscription to a stream occurs and that all events are received.
*
* @throws Throwable
*/
@Test
public void testGroupedEventStream() throws Throwable {
final AtomicInteger eventCounter = new AtomicInteger();
final AtomicInteger subscribeCounter = new AtomicInteger();
final AtomicInteger groupCounter = new AtomicInteger();
final CountDownLatch latch = new CountDownLatch(1);
final int count = 100;
final int groupCount = 2;
Observable<Event> es = Observable.create(new Observable.OnSubscribe<Event>() {
@Override
public void call(final Subscriber<? super Event> observer) {
System.out.println("*** Subscribing to EventStream ***");
subscribeCounter.incrementAndGet();
new Thread(new Runnable() {
@Override
public void run() {
for (int i = 0; i < count; i++) {
Event e = new Event();
e.source = i % groupCount;
e.message = "Event-" + i;
observer.onNext(e);
}
observer.onCompleted();
}
}).start();
}
});
es.groupBy(new Func1<Event, Integer>() {
@Override
public Integer call(Event e) {
return e.source;
}
}).flatMap(new Func1<GroupedObservable<Integer, Event>, Observable<String>>() {
@Override
public Observable<String> call(GroupedObservable<Integer, Event> eventGroupedObservable) {
System.out.println("GroupedObservable Key: " + eventGroupedObservable.getKey());
groupCounter.incrementAndGet();
return eventGroupedObservable.map(new Func1<Event, String>() {
@Override
public String call(Event event) {
return "Source: " + event.source + " Message: " + event.message;
}
});
}
}).subscribe(new Subscriber<String>() {
@Override
public void onCompleted() {
latch.countDown();
}
@Override
public void onError(Throwable e) {
e.printStackTrace();
latch.countDown();
}
@Override
public void onNext(String outputMessage) {
System.out.println(outputMessage);
eventCounter.incrementAndGet();
}
});
latch.await(5000, TimeUnit.MILLISECONDS);
assertEquals(1, subscribeCounter.get());
assertEquals(groupCount, groupCounter.get());
assertEquals(count, eventCounter.get());
}
/*
* We will only take 1 group with 20 events from it and then unsubscribe.
*/
@Test
public void testUnsubscribeOnNestedTakeAndSyncInfiniteStream() throws InterruptedException {
final AtomicInteger subscribeCounter = new AtomicInteger();
final AtomicInteger sentEventCounter = new AtomicInteger();
doTestUnsubscribeOnNestedTakeAndAsyncInfiniteStream(SYNC_INFINITE_OBSERVABLE_OF_EVENT(2, subscribeCounter, sentEventCounter), subscribeCounter);
Thread.sleep(500);
assertEquals(39, sentEventCounter.get());
}
/*
* We will only take 1 group with 20 events from it and then unsubscribe.
*/
@Test
public void testUnsubscribeOnNestedTakeAndAsyncInfiniteStream() throws InterruptedException {
final AtomicInteger subscribeCounter = new AtomicInteger();
final AtomicInteger sentEventCounter = new AtomicInteger();
doTestUnsubscribeOnNestedTakeAndAsyncInfiniteStream(ASYNC_INFINITE_OBSERVABLE_OF_EVENT(2, subscribeCounter, sentEventCounter), subscribeCounter);
Thread.sleep(500);
assertEquals(39, sentEventCounter.get());
}
private void doTestUnsubscribeOnNestedTakeAndAsyncInfiniteStream(Observable<Event> es, AtomicInteger subscribeCounter) throws InterruptedException {
final AtomicInteger eventCounter = new AtomicInteger();
final AtomicInteger groupCounter = new AtomicInteger();
final CountDownLatch latch = new CountDownLatch(1);
es.groupBy(new Func1<Event, Integer>() {
@Override
public Integer call(Event e) {
return e.source;
}
})
.take(1) // we want only the first group
.flatMap(new Func1<GroupedObservable<Integer, Event>, Observable<String>>() {
@Override
public Observable<String> call(GroupedObservable<Integer, Event> eventGroupedObservable) {
System.out.println("testUnsubscribe => GroupedObservable Key: " + eventGroupedObservable.getKey());
groupCounter.incrementAndGet();
return eventGroupedObservable
.take(20) // limit to only 20 events on this group
.map(new Func1<Event, String>() {
@Override
public String call(Event event) {
return "testUnsubscribe => Source: " + event.source + " Message: " + event.message;
}
});
}
}).subscribe(new Subscriber<String>() {
@Override
public void onCompleted() {
latch.countDown();
}
@Override
public void onError(Throwable e) {
e.printStackTrace();
latch.countDown();
}
@Override
public void onNext(String outputMessage) {
System.out.println(outputMessage);
eventCounter.incrementAndGet();
}
});
if (!latch.await(2000, TimeUnit.MILLISECONDS)) {
fail("timed out so likely did not unsubscribe correctly");
}
assertEquals(1, subscribeCounter.get());
assertEquals(1, groupCounter.get());
assertEquals(20, eventCounter.get());
// sentEvents will go until 'eventCounter' hits 20 and then unsubscribes
// which means it will also send (but ignore) the 19/20 events for the other group
// It will not however send all 100 events.
}
@Test
public void testUnsubscribeViaTakeOnGroupThenMergeAndTake() {
final AtomicInteger subscribeCounter = new AtomicInteger();
final AtomicInteger sentEventCounter = new AtomicInteger();
final AtomicInteger eventCounter = new AtomicInteger();
SYNC_INFINITE_OBSERVABLE_OF_EVENT(4, subscribeCounter, sentEventCounter)
.groupBy(new Func1<Event, Integer>() {
@Override
public Integer call(Event e) {
return e.source;
}
})
// take 2 of the 4 groups
.take(2)
.flatMap(new Func1<GroupedObservable<Integer, Event>, Observable<String>>() {
@Override
public Observable<String> call(GroupedObservable<Integer, Event> eventGroupedObservable) {
return eventGroupedObservable
.map(new Func1<Event, String>() {
@Override
public String call(Event event) {
return "testUnsubscribe => Source: " + event.source + " Message: " + event.message;
}
});
}
})
.take(30).subscribe(new Action1<String>() {
@Override
public void call(String s) {
eventCounter.incrementAndGet();
System.out.println("=> " + s);
}
});
assertEquals(30, eventCounter.get());
// we should send 28 additional events that are filtered out as they are in the groups we skip
assertEquals(58, sentEventCounter.get());
}
@Test
public void testUnsubscribeViaTakeOnGroupThenTakeOnInner() {
final AtomicInteger subscribeCounter = new AtomicInteger();
final AtomicInteger sentEventCounter = new AtomicInteger();
final AtomicInteger eventCounter = new AtomicInteger();
SYNC_INFINITE_OBSERVABLE_OF_EVENT(4, subscribeCounter, sentEventCounter)
.groupBy(new Func1<Event, Integer>() {
@Override
public Integer call(Event e) {
return e.source;
}
})
// take 2 of the 4 groups
.take(2)
.flatMap(new Func1<GroupedObservable<Integer, Event>, Observable<String>>() {
@Override
public Observable<String> call(GroupedObservable<Integer, Event> eventGroupedObservable) {
int numToTake = 0;
if (eventGroupedObservable.getKey() == 1) {
numToTake = 10;
} else if (eventGroupedObservable.getKey() == 2) {
numToTake = 5;
}
return eventGroupedObservable
.take(numToTake)
.map(new Func1<Event, String>() {
@Override
public String call(Event event) {
return "testUnsubscribe => Source: " + event.source + " Message: " + event.message;
}
});
}
})
.subscribe(new Action1<String>() {
@Override
public void call(String s) {
eventCounter.incrementAndGet();
System.out.println("=> " + s);
}
});
assertEquals(15, eventCounter.get());
// we should send 22 additional events that are filtered out as they are skipped while taking the 15 we want
assertEquals(37, sentEventCounter.get());
}
@Test
public void testStaggeredCompletion() throws InterruptedException {
final AtomicInteger eventCounter = new AtomicInteger();
final CountDownLatch latch = new CountDownLatch(1);
Observable.range(0, 100)
.groupBy(new Func1<Integer, Integer>() {
@Override
public Integer call(Integer i) {
return i % 2;
}
})
.flatMap(new Func1<GroupedObservable<Integer, Integer>, Observable<Integer>>() {
@Override
public Observable<Integer> call(GroupedObservable<Integer, Integer> group) {
if (group.getKey() == 0) {
return group.delay(100, TimeUnit.MILLISECONDS).map(new Func1<Integer, Integer>() {
@Override
public Integer call(Integer t) {
return t * 10;
}
});
} else {
return group;
}
}
})
.subscribe(new Subscriber<Integer>() {
@Override
public void onCompleted() {
System.out.println("=> onCompleted");
latch.countDown();
}
@Override
public void onError(Throwable e) {
e.printStackTrace();
latch.countDown();
}
@Override
public void onNext(Integer s) {
eventCounter.incrementAndGet();
System.out.println("=> " + s);
}
});
if (!latch.await(3000, TimeUnit.MILLISECONDS)) {
fail("timed out");
}
assertEquals(100, eventCounter.get());
}
@Test(timeout = 1000)
public void testCompletionIfInnerNotSubscribed() throws InterruptedException {
final CountDownLatch latch = new CountDownLatch(1);
final AtomicInteger eventCounter = new AtomicInteger();
Observable.range(0, 100)
.groupBy(new Func1<Integer, Integer>() {
@Override
public Integer call(Integer i) {
return i % 2;
}
})
.subscribe(new Subscriber<GroupedObservable<Integer, Integer>>() {
@Override
public void onCompleted() {
latch.countDown();
}
@Override
public void onError(Throwable e) {
e.printStackTrace();
latch.countDown();
}
@Override
public void onNext(GroupedObservable<Integer, Integer> s) {
eventCounter.incrementAndGet();
System.out.println("=> " + s);
}
});
if (!latch.await(500, TimeUnit.MILLISECONDS)) {
fail("timed out - never got completion");
}
assertEquals(2, eventCounter.get());
}
@Test
public void testIgnoringGroups() {
final AtomicInteger subscribeCounter = new AtomicInteger();
final AtomicInteger sentEventCounter = new AtomicInteger();
final AtomicInteger eventCounter = new AtomicInteger();
SYNC_INFINITE_OBSERVABLE_OF_EVENT(4, subscribeCounter, sentEventCounter)
.groupBy(new Func1<Event, Integer>() {
@Override
public Integer call(Event e) {
return e.source;
}
})
.flatMap(new Func1<GroupedObservable<Integer, Event>, Observable<String>>() {
@Override
public Observable<String> call(GroupedObservable<Integer, Event> eventGroupedObservable) {
Observable<Event> eventStream = eventGroupedObservable;
if (eventGroupedObservable.getKey() >= 2) {
// filter these
eventStream = eventGroupedObservable.filter(new Func1<Event, Boolean>() {
@Override
public Boolean call(Event t1) {
return false;
}
});
}
return eventStream
.map(new Func1<Event, String>() {
@Override
public String call(Event event) {
return "testUnsubscribe => Source: " + event.source + " Message: " + event.message;
}
});
}
})
.take(30).subscribe(new Action1<String>() {
@Override
public void call(String s) {
eventCounter.incrementAndGet();
System.out.println("=> " + s);
}
});
assertEquals(30, eventCounter.get());
// we should send 30 additional events that are filtered out as they are in the groups we skip
assertEquals(60, sentEventCounter.get());
}
@Test
public void testFirstGroupsCompleteAndParentSlowToThenEmitFinalGroupsAndThenComplete() throws InterruptedException {
final CountDownLatch first = new CountDownLatch(2); // there are two groups to first complete
final ArrayList<String> results = new ArrayList<String>();
Observable.create(new OnSubscribe<Integer>() {
@Override
public void call(Subscriber<? super Integer> sub) {
sub.onNext(1);
sub.onNext(2);
sub.onNext(1);
sub.onNext(2);
try {
first.await();
} catch (InterruptedException e) {
sub.onError(e);
return;
}
sub.onNext(3);
sub.onNext(3);
sub.onCompleted();
}
}).groupBy(new Func1<Integer, Integer>() {
@Override
public Integer call(Integer t) {
return t;
}
}).flatMap(new Func1<GroupedObservable<Integer, Integer>, Observable<String>>() {
@Override
public Observable<String> call(final GroupedObservable<Integer, Integer> group) {
if (group.getKey() < 3) {
return group.map(new Func1<Integer, String>() {
@Override
public String call(Integer t1) {
return "first groups: " + t1;
}
})
// must take(2) so an onCompleted + unsubscribe happens on these first 2 groups
.take(2).doOnCompleted(new Action0() {
@Override
public void call() {
first.countDown();
}
});
} else {
return group.map(new Func1<Integer, String>() {
@Override
public String call(Integer t1) {
return "last group: " + t1;
}
});
}
}
}).toBlocking().forEach(new Action1<String>() {
@Override
public void call(String s) {
results.add(s);
}
});
System.out.println("Results: " + results);
assertEquals(6, results.size());
}
@Test
public void testFirstGroupsCompleteAndParentSlowToThenEmitFinalGroupsWhichThenSubscribesOnAndDelaysAndThenCompletes() throws InterruptedException {
System.err.println("----------------------------------------------------------------------------------------------");
final CountDownLatch first = new CountDownLatch(2); // there are two groups to first complete
final ArrayList<String> results = new ArrayList<String>();
Observable.create(new OnSubscribe<Integer>() {
@Override
public void call(Subscriber<? super Integer> sub) {
sub.onNext(1);
sub.onNext(2);
sub.onNext(1);
sub.onNext(2);
try {
first.await();
} catch (InterruptedException e) {
sub.onError(e);
return;
}
sub.onNext(3);
sub.onNext(3);
sub.onCompleted();
}
}).groupBy(new Func1<Integer, Integer>() {
@Override
public Integer call(Integer t) {
return t;
}
}).flatMap(new Func1<GroupedObservable<Integer, Integer>, Observable<String>>() {
@Override
public Observable<String> call(final GroupedObservable<Integer, Integer> group) {
if (group.getKey() < 3) {
return group.map(new Func1<Integer, String>() {
@Override
public String call(Integer t1) {
return "first groups: " + t1;
}
})
// must take(2) so an onCompleted + unsubscribe happens on these first 2 groups
.take(2).doOnCompleted(new Action0() {
@Override
public void call() {
first.countDown();
}
});
} else {
return group.subscribeOn(Schedulers.newThread()).delay(400, TimeUnit.MILLISECONDS).map(new Func1<Integer, String>() {
@Override
public String call(Integer t1) {
return "last group: " + t1;
}
}).doOnEach(new Action1<Notification<? super String>>() {
@Override
public void call(Notification<? super String> t1) {
System.err.println("subscribeOn notification => " + t1);
}
});
}
}
}).doOnEach(new Action1<Notification<? super String>>() {
@Override
public void call(Notification<? super String> t1) {
System.err.println("outer notification => " + t1);
}
}).toBlocking().forEach(new Action1<String>() {
@Override
public void call(String s) {
results.add(s);
}
});
System.out.println("Results: " + results);
assertEquals(6, results.size());
}
@Test
public void testFirstGroupsCompleteAndParentSlowToThenEmitFinalGroupsWhichThenObservesOnAndDelaysAndThenCompletes() throws InterruptedException {
final CountDownLatch first = new CountDownLatch(2); // there are two groups to first complete
final ArrayList<String> results = new ArrayList<String>();
Observable.create(new OnSubscribe<Integer>() {
@Override
public void call(Subscriber<? super Integer> sub) {
sub.onNext(1);
sub.onNext(2);
sub.onNext(1);
sub.onNext(2);
try {
first.await();
} catch (InterruptedException e) {
sub.onError(e);
return;
}
sub.onNext(3);
sub.onNext(3);
sub.onCompleted();
}
}).groupBy(new Func1<Integer, Integer>() {
@Override
public Integer call(Integer t) {
return t;
}
}).flatMap(new Func1<GroupedObservable<Integer, Integer>, Observable<String>>() {
@Override
public Observable<String> call(final GroupedObservable<Integer, Integer> group) {
if (group.getKey() < 3) {
return group.map(new Func1<Integer, String>() {
@Override
public String call(Integer t1) {
return "first groups: " + t1;
}
})
// must take(2) so an onCompleted + unsubscribe happens on these first 2 groups
.take(2).doOnCompleted(new Action0() {
@Override
public void call() {
first.countDown();
}
});
} else {
return group.observeOn(Schedulers.newThread()).delay(400, TimeUnit.MILLISECONDS).map(new Func1<Integer, String>() {
@Override
public String call(Integer t1) {
return "last group: " + t1;
}
});
}
}
}).toBlocking().forEach(new Action1<String>() {
@Override
public void call(String s) {
results.add(s);
}
});
System.out.println("Results: " + results);
assertEquals(6, results.size());
}
@Test
public void testGroupsWithNestedSubscribeOn() throws InterruptedException {
final ArrayList<String> results = new ArrayList<String>();
Observable.create(new OnSubscribe<Integer>() {
@Override
public void call(Subscriber<? super Integer> sub) {
sub.onNext(1);
sub.onNext(2);
sub.onNext(1);
sub.onNext(2);
sub.onCompleted();
}
}).groupBy(new Func1<Integer, Integer>() {
@Override
public Integer call(Integer t) {
return t;
}
}).flatMap(new Func1<GroupedObservable<Integer, Integer>, Observable<String>>() {
@Override
public Observable<String> call(final GroupedObservable<Integer, Integer> group) {
return group.subscribeOn(Schedulers.newThread()).map(new Func1<Integer, String>() {
@Override
public String call(Integer t1) {
System.out.println("Received: " + t1 + " on group : " + group.getKey());
return "first groups: " + t1;
}
});
}
}).doOnEach(new Action1<Notification<? super String>>() {
@Override
public void call(Notification<? super String> t1) {
System.out.println("notification => " + t1);
}
}).toBlocking().forEach(new Action1<String>() {
@Override
public void call(String s) {
results.add(s);
}
});
System.out.println("Results: " + results);
assertEquals(4, results.size());
}
@Test
public void testGroupsWithNestedObserveOn() throws InterruptedException {
final ArrayList<String> results = new ArrayList<String>();
Observable.create(new OnSubscribe<Integer>() {
@Override
public void call(Subscriber<? super Integer> sub) {
sub.onNext(1);
sub.onNext(2);
sub.onNext(1);
sub.onNext(2);
sub.onCompleted();
}
}).groupBy(new Func1<Integer, Integer>() {
@Override
public Integer call(Integer t) {
return t;
}
}).flatMap(new Func1<GroupedObservable<Integer, Integer>, Observable<String>>() {
@Override
public Observable<String> call(final GroupedObservable<Integer, Integer> group) {
return group.observeOn(Schedulers.newThread()).delay(400, TimeUnit.MILLISECONDS).map(new Func1<Integer, String>() {
@Override
public String call(Integer t1) {
return "first groups: " + t1;
}
});
}
}).toBlocking().forEach(new Action1<String>() {
@Override
public void call(String s) {
results.add(s);
}
});
System.out.println("Results: " + results);
assertEquals(4, results.size());
}
private static class Event {
int source;
String message;
@Override
public String toString() {
return "Event => source: " + source + " message: " + message;
}
}
Observable<Event> ASYNC_INFINITE_OBSERVABLE_OF_EVENT(final int numGroups, final AtomicInteger subscribeCounter, final AtomicInteger sentEventCounter) {
return SYNC_INFINITE_OBSERVABLE_OF_EVENT(numGroups, subscribeCounter, sentEventCounter).subscribeOn(Schedulers.newThread());
};
Observable<Event> SYNC_INFINITE_OBSERVABLE_OF_EVENT(final int numGroups, final AtomicInteger subscribeCounter, final AtomicInteger sentEventCounter) {
return Observable.create(new OnSubscribe<Event>() {
@Override
public void call(final Subscriber<? super Event> op) {
subscribeCounter.incrementAndGet();
int i = 0;
while (!op.isUnsubscribed()) {
i++;
Event e = new Event();
e.source = i % numGroups;
e.message = "Event-" + i;
op.onNext(e);
sentEventCounter.incrementAndGet();
}
op.onCompleted();
}
});
};
@Test
public void testGroupByOnAsynchronousSourceAcceptsMultipleSubscriptions() throws InterruptedException {
// choose an asynchronous source
Observable<Long> source = Observable.interval(10, TimeUnit.MILLISECONDS).take(1);
// apply groupBy to the source
Observable<GroupedObservable<Boolean, Long>> stream = source.groupBy(IS_EVEN);
// create two observers
@SuppressWarnings("unchecked")
Observer<GroupedObservable<Boolean, Long>> o1 = mock(Observer.class);
@SuppressWarnings("unchecked")
Observer<GroupedObservable<Boolean, Long>> o2 = mock(Observer.class);
// subscribe with the observers
stream.subscribe(o1);
stream.subscribe(o2);
// check that subscriptions were successful
verify(o1, never()).onError(Matchers.<Throwable> any());
verify(o2, never()).onError(Matchers.<Throwable> any());
}
private static Func1<Long, Boolean> IS_EVEN = new Func1<Long, Boolean>() {
@Override
public Boolean call(Long n) {
return n % 2 == 0;
}
};
private static Func1<Integer, Boolean> IS_EVEN2 = new Func1<Integer, Boolean>() {
@Override
public Boolean call(Integer n) {
return n % 2 == 0;
}
};
@Test
public void testGroupByBackpressure() throws InterruptedException {
TestSubscriber<String> ts = new TestSubscriber<String>();
Observable.range(1, 4000)
.groupBy(IS_EVEN2)
.flatMap(new Func1<GroupedObservable<Boolean, Integer>, Observable<String>>() {
@Override
public Observable<String> call(final GroupedObservable<Boolean, Integer> g) {
return g.observeOn(Schedulers.computation()).map(new Func1<Integer, String>() {
@Override
public String call(Integer l) {
if (g.getKey()) {
try {
Thread.sleep(1);
} catch (InterruptedException e) {
}
return l + " is even.";
} else {
return l + " is odd.";
}
}
});
}
}).subscribe(ts);
ts.awaitTerminalEvent();
ts.assertNoErrors();
}
<T, R> Func1<T, R> just(final R value) {
return new Func1<T, R>() {
@Override
public R call(T t1) {
return value;
}
};
}
<T> Func1<Integer, T> fail(T dummy) {
return new Func1<Integer, T>() {
@Override
public T call(Integer t1) {
throw new RuntimeException("Forced failure");
}
};
}
<T, R> Func1<T, R> fail2(R dummy2) {
return new Func1<T, R>() {
@Override
public R call(T t1) {
throw new RuntimeException("Forced failure");
}
};
}
Func1<Integer, Integer> dbl = new Func1<Integer, Integer>() {
@Override
public Integer call(Integer t1) {
return t1 * 2;
}
};
Func1<Integer, Integer> identity = UtilityFunctions.identity();
@Before
public void before() {
MockitoAnnotations.initMocks(this);
}
@Test
public void normalBehavior() {
Observable<String> source = Observable.from(Arrays.asList(
" foo",
" FoO ",
"baR ",
"foO ",
" Baz ",
" qux ",
" bar",
" BAR ",
"FOO ",
"baz ",
" bAZ ",
" fOo "
));
/**
* foo FoO foO FOO fOo
* baR bar BAR
* Baz baz bAZ
* qux
*
*/
Func1<String, String> keysel = new Func1<String, String>() {
@Override
public String call(String t1) {
return t1.trim().toLowerCase();
}
};
Func1<String, String> valuesel = new Func1<String, String>() {
@Override
public String call(String t1) {
return t1 + t1;
}
};
Observable<String> m = source.groupBy(
keysel, valuesel).flatMap(new Func1<GroupedObservable<String, String>, Observable<String>>() {
@Override
public Observable<String> call(final GroupedObservable<String, String> g) {
System.out.println("-----------> NEXT: " + g.getKey());
return g.take(2).map(new Func1<String, String>() {
int count = 0;
@Override
public String call(String v) {
return g.getKey() + "-" + count++;
}
});
}
});
TestSubscriber<String> ts = new TestSubscriber<String>();
m.subscribe(ts);
ts.awaitTerminalEvent();
System.out.println("ts .get " + ts.getOnNextEvents());
ts.assertNoErrors();
assertEquals(ts.getOnNextEvents(),
Arrays.asList("foo-0", "foo-1", "bar-0", "foo-0", "baz-0", "qux-0", "bar-1", "bar-0", "foo-1", "baz-1", "baz-0", "foo-0"));
}
@Test
public void keySelectorThrows() {
Observable<Integer> source = Observable.just(0, 1, 2, 3, 4, 5, 6);
Observable<Integer> m = source.groupBy(fail(0), dbl).flatMap(FLATTEN_INTEGER);
TestSubscriber<Integer> ts = new TestSubscriber<Integer>();
m.subscribe(ts);
ts.awaitTerminalEvent();
assertEquals(1, ts.getOnErrorEvents().size());
assertEquals(0, ts.getOnNextEvents().size());
}
@Test
public void valueSelectorThrows() {
Observable<Integer> source = Observable.just(0, 1, 2, 3, 4, 5, 6);
Observable<Integer> m = source.groupBy(identity, fail(0)).flatMap(FLATTEN_INTEGER);
TestSubscriber<Integer> ts = new TestSubscriber<Integer>();
m.subscribe(ts);
ts.awaitTerminalEvent();
assertEquals(1, ts.getOnErrorEvents().size());
assertEquals(0, ts.getOnNextEvents().size());
}
@Test
public void innerEscapeCompleted() {
Observable<Integer> source = Observable.just(0);
Observable<Integer> m = source.groupBy(identity, dbl).flatMap(FLATTEN_INTEGER);
TestSubscriber<Object> ts = new TestSubscriber<Object>();
m.subscribe(ts);
ts.awaitTerminalEvent();
ts.assertNoErrors();
System.out.println(ts.getOnNextEvents());
}
/**
* Assert we get an IllegalStateException if trying to subscribe to an inner GroupedObservable more than once
*/
@Test
public void testExceptionIfSubscribeToChildMoreThanOnce() {
Observable<Integer> source = Observable.just(0);
final AtomicReference<GroupedObservable<Integer, Integer>> inner = new AtomicReference<GroupedObservable<Integer, Integer>>();
Observable<GroupedObservable<Integer, Integer>> m = source.groupBy(identity, dbl);
m.subscribe(new Action1<GroupedObservable<Integer, Integer>>() {
@Override
public void call(GroupedObservable<Integer, Integer> t1) {
inner.set(t1);
}
});
inner.get().subscribe();
@SuppressWarnings("unchecked")
Observer<Integer> o2 = mock(Observer.class);
inner.get().subscribe(o2);
verify(o2, never()).onCompleted();
verify(o2, never()).onNext(anyInt());
verify(o2).onError(any(IllegalStateException.class));
}
@Test
public void testError2() {
Observable<Integer> source = Observable.concat(Observable.just(0),
Observable.<Integer> error(new TestException("Forced failure")));
Observable<Integer> m = source.groupBy(identity, dbl).flatMap(FLATTEN_INTEGER);
TestSubscriber<Object> ts = new TestSubscriber<Object>();
m.subscribe(ts);
ts.awaitTerminalEvent();
assertEquals(1, ts.getOnErrorEvents().size());
assertEquals(1, ts.getOnNextEvents().size());
}
@Test
public void testgroupByBackpressure() throws InterruptedException {
TestSubscriber<String> ts = new TestSubscriber<String>();
Observable.range(1, 4000).groupBy(IS_EVEN2).flatMap(new Func1<GroupedObservable<Boolean, Integer>, Observable<String>>() {
@Override
public Observable<String> call(final GroupedObservable<Boolean, Integer> g) {
return g.doOnCompleted(new Action0() {
@Override
public void call() {
System.out.println("//////////////////// COMPLETED-A");
}
}).observeOn(Schedulers.computation()).map(new Func1<Integer, String>() {
int c = 0;
@Override
public String call(Integer l) {
if (g.getKey()) {
if (c++ < 400) {
try {
Thread.sleep(1);
} catch (InterruptedException e) {
}
}
return l + " is even.";
} else {
return l + " is odd.";
}
}
}).doOnCompleted(new Action0() {
@Override
public void call() {
System.out.println("//////////////////// COMPLETED-B");
}
});
}
}).doOnEach(new Action1<Notification<? super String>>() {
@Override
public void call(Notification<? super String> t1) {
System.out.println("NEXT: " + t1);
}
}).subscribe(ts);
ts.awaitTerminalEvent();
ts.assertNoErrors();
}
@Test
public void testgroupByBackpressure2() throws InterruptedException {
TestSubscriber<String> ts = new TestSubscriber<String>();
Observable.range(1, 4000).groupBy(IS_EVEN2).flatMap(new Func1<GroupedObservable<Boolean, Integer>, Observable<String>>() {
@Override
public Observable<String> call(final GroupedObservable<Boolean, Integer> g) {
return g.take(2).observeOn(Schedulers.computation()).map(new Func1<Integer, String>() {
@Override
public String call(Integer l) {
if (g.getKey()) {
try {
Thread.sleep(1);
} catch (InterruptedException e) {
}
return l + " is even.";
} else {
return l + " is odd.";
}
}
});
}
}).subscribe(ts);
ts.awaitTerminalEvent();
ts.assertNoErrors();
}
static Func1<GroupedObservable<Integer, Integer>, Observable<Integer>> FLATTEN_INTEGER = new Func1<GroupedObservable<Integer, Integer>, Observable<Integer>>() {
@Override
public Observable<Integer> call(GroupedObservable<Integer, Integer> t) {
return t;
}
};
@Test
public void testGroupByWithNullKey() {
final String[] key = new String[]{"uninitialized"};
final List<String> values = new ArrayList<String>();
Observable.just("a", "b", "c").groupBy(new Func1<String, String>() {
@Override
public String call(String value) {
return null;
}
}).subscribe(new Action1<GroupedObservable<String, String>>() {
@Override
public void call(GroupedObservable<String, String> groupedObservable) {
key[0] = groupedObservable.getKey();
groupedObservable.subscribe(new Action1<String>() {
@Override
public void call(String s) {
values.add(s);
}
});
}
});
assertEquals(null, key[0]);
assertEquals(Arrays.asList("a", "b", "c"), values);
}
@Test
public void testGroupByUnsubscribe() {
final Subscription s = mock(Subscription.class);
Observable<Integer> o = Observable.create(
new OnSubscribe<Integer>() {
@Override
public void call(Subscriber<? super Integer> subscriber) {
subscriber.add(s);
}
}
);
o.groupBy(new Func1<Integer, Integer>() {
@Override
public Integer call(Integer integer) {
return null;
}
}).subscribe().unsubscribe();
verify(s).unsubscribe();
}
@Test
public void testGroupByShouldPropagateError() {
final Throwable e = new RuntimeException("Oops");
final TestSubscriber<Integer> inner1 = new TestSubscriber<Integer>();
final TestSubscriber<Integer> inner2 = new TestSubscriber<Integer>();
final TestSubscriber<GroupedObservable<Integer, Integer>> outer
= new TestSubscriber<GroupedObservable<Integer, Integer>>(new Subscriber<GroupedObservable<Integer, Integer>>() {
@Override
public void onCompleted() {
}
@Override
public void onError(Throwable e) {
}
@Override
public void onNext(GroupedObservable<Integer, Integer> o) {
if (o.getKey() == 0) {
o.subscribe(inner1);
} else {
o.subscribe(inner2);
}
}
});
Observable.create(
new OnSubscribe<Integer>() {
@Override
public void call(Subscriber<? super Integer> subscriber) {
subscriber.onNext(0);
subscriber.onNext(1);
subscriber.onError(e);
}
}
).groupBy(new Func1<Integer, Integer>() {
@Override
public Integer call(Integer i) {
return i % 2;
}
}).subscribe(outer);
assertEquals(Arrays.asList(e), outer.getOnErrorEvents());
assertEquals(Arrays.asList(e), inner1.getOnErrorEvents());
assertEquals(Arrays.asList(e), inner2.getOnErrorEvents());
}
}