/*
* Copyright (c) 2011-2016 Pivotal Software Inc, All Rights Reserved.
*
* 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 reactor.core.publisher;
import java.util.AbstractCollection;
import java.util.Arrays;
import java.util.HashSet;
import java.util.Iterator;
import java.util.List;
import java.util.Objects;
import org.junit.Test;
import reactor.core.Fuseable;
import reactor.test.StepVerifier;
import reactor.test.publisher.FluxOperatorTest;
import reactor.test.subscriber.AssertSubscriber;
public class FluxDistinctTest extends FluxOperatorTest<String, String> {
@Override
protected Scenario<String, String> defaultScenarioOptions(Scenario<String, String> defaultOptions) {
return defaultOptions.fusionMode(Fuseable.ANY)
.fusionModeThreadBarrier(Fuseable.ANY);
}
@Override
protected List<Scenario<String, String>> scenarios_operatorError() {
return Arrays.asList(
scenario(f -> f.distinct(d -> {
throw exception();
})),
scenario(f -> f.distinct(d -> null))
);
}
@Override
protected List<Scenario<String, String>> scenarios_errorFromUpstreamFailure() {
return Arrays.asList(
scenario(f -> f.distinct()));
}
@Override
protected List<Scenario<String, String>> scenarios_operatorSuccess() {
return Arrays.asList(
scenario(f -> f.distinct()).producer(3, i -> item(0))
.receiveValues((item(0)))
.receiverDemand(2),
scenario(f -> f.distinct())
);
}
@Test(expected = NullPointerException.class)
public void sourceNull() {
new FluxDistinct<>(null, k -> k, HashSet::new);
}
@Test(expected = NullPointerException.class)
public void keyExtractorNull() {
Flux.never().distinct(null);
}
@Test(expected = NullPointerException.class)
public void collectionSupplierNull() {
new FluxDistinct<>(Flux.never(), k -> k, null);
}
@Test
public void allDistinct() {
AssertSubscriber<Integer> ts = AssertSubscriber.create();
Flux.range(1, 10)
.distinct(k -> k)
.subscribe(ts);
ts.assertValues(1, 2, 3, 4, 5, 6, 7, 8, 9, 10)
.assertComplete()
.assertNoError();
}
@Test
public void allDistinctBackpressured() {
AssertSubscriber<Integer> ts = AssertSubscriber.create(0);
Flux.range(1, 10)
.distinct(k -> k)
.subscribe(ts);
ts.assertNoValues()
.assertNoError()
.assertNotComplete();
ts.request(2);
ts.assertValues(1, 2)
.assertNoError()
.assertNotComplete();
ts.request(5);
ts.assertValues(1, 2, 3, 4, 5, 6, 7)
.assertNoError()
.assertNotComplete();
ts.request(10);
ts.assertValues(1, 2, 3, 4, 5, 6, 7, 8, 9, 10)
.assertComplete()
.assertNoError();
}
@Test
public void allDistinctHide() {
AssertSubscriber<Integer> ts = AssertSubscriber.create();
Flux.range(1, 10)
.hide()
.distinct(k -> k)
.subscribe(ts);
ts.assertValues(1, 2, 3, 4, 5, 6, 7, 8, 9, 10)
.assertComplete()
.assertNoError();
}
@Test
public void allDistinctBackpressuredHide() {
AssertSubscriber<Integer> ts = AssertSubscriber.create(0);
Flux.range(1, 10)
.hide()
.distinct(k -> k)
.subscribe(ts);
ts.assertNoValues()
.assertNoError()
.assertNotComplete();
ts.request(2);
ts.assertValues(1, 2)
.assertNoError()
.assertNotComplete();
ts.request(5);
ts.assertValues(1, 2, 3, 4, 5, 6, 7)
.assertNoError()
.assertNotComplete();
ts.request(10);
ts.assertValues(1, 2, 3, 4, 5, 6, 7, 8, 9, 10)
.assertComplete()
.assertNoError();
}
@Test
public void someDistinct() {
AssertSubscriber<Integer> ts = AssertSubscriber.create();
Flux.just(1, 2, 2, 3, 4, 5, 6, 1, 2, 7, 7, 8, 9, 9, 10, 10, 10)
.distinct(k -> k)
.subscribe(ts);
ts.assertValues(1, 2, 3, 4, 5, 6, 7, 8, 9, 10)
.assertComplete()
.assertNoError();
}
@Test
public void someDistinctBackpressured() {
AssertSubscriber<Integer> ts = AssertSubscriber.create(0);
Flux.just(1, 2, 2, 3, 4, 5, 6, 1, 2, 7, 7, 8, 9, 9, 10, 10, 10)
.distinct(k -> k)
.subscribe(ts);
ts.assertNoValues()
.assertNoError()
.assertNotComplete();
ts.request(2);
ts.assertValues(1, 2)
.assertNoError()
.assertNotComplete();
ts.request(5);
ts.assertValues(1, 2, 3, 4, 5, 6, 7)
.assertNoError()
.assertNotComplete();
ts.request(10);
ts.assertValues(1, 2, 3, 4, 5, 6, 7, 8, 9, 10)
.assertComplete()
.assertNoError();
}
@Test
public void allSame() {
AssertSubscriber<Integer> ts = AssertSubscriber.create();
Flux.just(1, 1, 1, 1, 1, 1, 1, 1, 1)
.distinct(k -> k)
.subscribe(ts);
ts.assertValues(1)
.assertComplete()
.assertNoError();
}
@Test
public void allSameFusable() {
AssertSubscriber<Integer> ts = AssertSubscriber.create();
ts.requestedFusionMode(Fuseable.ANY);
Flux.just(1, 1, 1, 1, 1, 1, 1, 1, 1)
.distinct(k -> k)
.filter(t -> true)
.map(t -> t)
.cache(4)
.subscribe(ts);
ts.assertValues(1)
.assertFuseableSource()
.assertFusionEnabled()
.assertFusionMode(Fuseable.ASYNC)
.assertComplete()
.assertNoError();
}
@Test
public void allSameBackpressured() {
AssertSubscriber<Integer> ts = AssertSubscriber.create(0);
Flux.just(1, 1, 1, 1, 1, 1, 1, 1, 1)
.distinct(k -> k)
.subscribe(ts);
ts.assertNoValues()
.assertNoError()
.assertNotComplete();
ts.request(2);
ts.assertValues(1)
.assertComplete()
.assertNoError();
}
@Test
public void withKeyExtractorSame() {
AssertSubscriber<Integer> ts = AssertSubscriber.create();
Flux.range(1, 10).distinct(k -> k % 3).subscribe(ts);
ts.assertValues(1, 2, 3)
.assertComplete()
.assertNoError();
}
@Test
public void withKeyExtractorBackpressured() {
AssertSubscriber<Integer> ts = AssertSubscriber.create(0);
Flux.range(1, 10).distinct(k -> k % 3).subscribe(ts);
ts.assertNoValues()
.assertNoError()
.assertNotComplete();
ts.request(2);
ts.assertValues(1, 2)
.assertNotComplete()
.assertNoError();
ts.request(2);
ts.assertValues(1, 2, 3)
.assertComplete()
.assertNoError();
}
@Test
public void keyExtractorThrows() {
AssertSubscriber<Integer> ts = AssertSubscriber.create();
Flux.range(1, 10).distinct(k -> {
throw new RuntimeException("forced failure");
}).subscribe(ts);
ts.assertNoValues()
.assertNotComplete()
.assertError(RuntimeException.class)
.assertErrorMessage("forced failure");
}
@Test
public void collectionSupplierThrows() {
AssertSubscriber<Integer> ts = AssertSubscriber.create();
new FluxDistinct<>(Flux.range(1, 10), k -> k, () -> {
throw new RuntimeException("forced failure");
}).subscribe(ts);
ts.assertNoValues()
.assertNotComplete()
.assertError(RuntimeException.class)
.assertErrorMessage("forced failure");
}
@Test
public void collectionSupplierReturnsNull() {
AssertSubscriber<Integer> ts = AssertSubscriber.create();
new FluxDistinct<>(Flux.range(1, 10), k -> k, () -> null).subscribe(ts);
ts.assertNoValues()
.assertNotComplete()
.assertError(NullPointerException.class);
}
@Test
//see https://github.com/reactor/reactor-core/issues/577
public void collectionSupplierLimitedFifo() {
Flux<Integer> flux = Flux.just(1, 2, 3, 4, 5, 6, 1, 3, 4, 1, 1, 1, 1, 2);
StepVerifier.create(flux.distinct(Flux.identityFunction(), () -> new NaiveFifoQueue<>(5)))
.expectNext(1, 2, 3, 4, 5, 6, 1, 2)
.verifyComplete();
StepVerifier.create(flux.distinct(Flux.identityFunction(),
() -> new NaiveFifoQueue<>(3)))
.expectNext(1, 2, 3, 4, 5, 6, 1, 3, 4, 2)
.verifyComplete();
}
private static final class NaiveFifoQueue<T> extends AbstractCollection<T> {
final int limit;
int size = 0;
Object[] array;
public NaiveFifoQueue(int limit) {
this.limit = limit;
this.array = new Object[limit];
}
@Override
public Iterator<T> iterator() {
return null;
}
@Override
public void clear() {
this.array = new Object[limit];
this.size = 0;
}
@Override
public int size() {
return size;
}
@Override
public boolean add(T t) {
Objects.requireNonNull(t);
for (int i = 0; i < array.length; i++) {
if (array[i] == null) {
array[i] = t;
size++;
return true;
} else if (t.equals(array[i])) {
return false;
}
}
//at this point, no available slot and not a duplicate, drop oldest
Object[] old = array;
array = new Object[limit];
System.arraycopy(old, 1, array, 0, array.length - 1);
array[array.length - 1] = t;
//size doesn't change
return true;
}
}
}