blob: bf2721e9af695b5d22236446a174898e3789275c [file] [log] [blame]
/*
* Licensed to the Apache Software Foundation (ASF) under one or more
* contributor license agreements. See the NOTICE file distributed with
* this work for additional information regarding copyright ownership.
* The ASF licenses this file to You 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 org.apache.commons.rng.core;
import java.util.HashSet;
import java.util.Set;
import java.util.Spliterator;
import java.util.SplittableRandom;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.ThreadLocalRandom;
import java.util.function.Consumer;
import java.util.function.UnaryOperator;
import java.util.stream.Stream;
import org.apache.commons.rng.SplittableUniformRandomProvider;
import org.apache.commons.rng.UniformRandomProvider;
import org.apache.commons.rng.core.util.RandomStreamsTestHelper;
import org.junit.jupiter.api.Assertions;
import org.junit.jupiter.params.ParameterizedTest;
import org.junit.jupiter.params.provider.MethodSource;
/**
* Tests which all {@link SplittableUniformRandomProvider} generators must pass.
*/
class SplittableProvidersParametricTest {
/** The expected characteristics for the spliterator from the splittable stream. */
private static final int SPLITERATOR_CHARACTERISTICS =
Spliterator.SIZED | Spliterator.SUBSIZED | Spliterator.IMMUTABLE;
/**
* Dummy class for checking the behavior of the SplittableUniformRandomProvider.
* All generation and split methods throw an exception. This can be used to test
* exception conditions for arguments to default stream functions.
*/
private static class DummyGenerator implements SplittableUniformRandomProvider {
/** An instance. */
static final DummyGenerator INSTANCE = new DummyGenerator();
@Override
public long nextLong() {
throw new UnsupportedOperationException("The nextLong method should not be invoked");
}
@Override
public SplittableUniformRandomProvider split(UniformRandomProvider source) {
throw new UnsupportedOperationException("The split(source) method should not be invoked");
}
}
/**
* Thread-safe class for checking the behavior of the SplittableUniformRandomProvider.
* Generation methods default to ThreadLocalRandom. Split methods return the same instance.
* This is a functioning generator that can be used as a source to seed splitting.
*/
private static class ThreadLocalGenerator implements SplittableUniformRandomProvider {
/** An instance. */
static final ThreadLocalGenerator INSTANCE = new ThreadLocalGenerator();
@Override
public long nextLong() {
return ThreadLocalRandom.current().nextLong();
}
@Override
public SplittableUniformRandomProvider split(UniformRandomProvider source) {
return this;
}
}
/**
* Gets the list of splittable generators.
*
* @return the list
*/
private static Iterable<SplittableUniformRandomProvider> getSplittableProviders() {
return ProvidersList.listSplittable();
}
/**
* Test that the split methods throw when the source of randomness is null.
*/
@ParameterizedTest
@MethodSource("getSplittableProviders")
void testSplitThrowsWithNullSource(SplittableUniformRandomProvider generator) {
Assertions.assertThrows(NullPointerException.class, () -> generator.split(null));
}
/**
* Test that the random generator returned from the split is a new instance of the same class.
*/
@ParameterizedTest
@MethodSource("getSplittableProviders")
void testSplitReturnsANewInstance(SplittableUniformRandomProvider generator) {
assertSplitReturnsANewInstance(SplittableUniformRandomProvider::split, generator);
}
/**
* Test that the random generator returned from the split(source) is a new instance of the same class.
*/
@ParameterizedTest
@MethodSource("getSplittableProviders")
void testSplitWithSourceReturnsANewInstance(SplittableUniformRandomProvider generator) {
assertSplitReturnsANewInstance(s -> s.split(ThreadLocalGenerator.INSTANCE), generator);
}
/**
* Assert that the random generator returned from the split function is a new instance of the same class.
*
* @param splitFunction Split function to test.
* @param generator RNG under test.
*/
private static void assertSplitReturnsANewInstance(UnaryOperator<SplittableUniformRandomProvider> splitFunction,
SplittableUniformRandomProvider generator) {
final UniformRandomProvider child = splitFunction.apply(generator);
Assertions.assertNotSame(generator, child, "The child instance should be a different object");
Assertions.assertEquals(generator.getClass(), child.getClass(), "The child instance should be the same class");
RandomAssert.assertNextLongNotEquals(10, generator, child);
}
/**
* Test that the split method is reproducible when used with the same generator source in the
* same state.
*/
@ParameterizedTest
@MethodSource("getSplittableProviders")
void testSplitWithSourceIsReproducible(SplittableUniformRandomProvider generator) {
final long seed = ThreadLocalRandom.current().nextLong();
UniformRandomProvider rng1 = generator.split(new SplittableRandom(seed)::nextLong);
UniformRandomProvider rng2 = generator.split(new SplittableRandom(seed)::nextLong);
RandomAssert.assertNextLongEquals(10, rng1, rng2);
}
/**
* Test that the other stream splits methods all call the
* {@link SplittableUniformRandomProvider#splits(long, SplittableUniformRandomProvider)} method.
* This is tested by checking the spliterator is the same.
*
* <p>This test serves to ensure the default implementations in SplittableUniformRandomProvider
* eventually call the same method. The RNG implementation thus only has to override one method.
*/
@ParameterizedTest
@MethodSource("getSplittableProviders")
void testSplitsMethodsUseSameSpliterator(SplittableUniformRandomProvider generator) {
final long size = 10;
final Spliterator<SplittableUniformRandomProvider> s = generator.splits(size, generator).spliterator();
Assertions.assertEquals(s.getClass(), generator.splits().spliterator().getClass());
Assertions.assertEquals(s.getClass(), generator.splits(size).spliterator().getClass());
Assertions.assertEquals(s.getClass(), generator.splits(ThreadLocalGenerator.INSTANCE).spliterator().getClass());
}
@ParameterizedTest
@MethodSource("getSplittableProviders")
void testSplitsSize(SplittableUniformRandomProvider generator) {
for (final long size : new long[] {0, 1, 7, 13}) {
Assertions.assertEquals(size, generator.splits(size).count(), "splits");
Assertions.assertEquals(size, generator.splits(size, ThreadLocalGenerator.INSTANCE).count(), "splits with source");
}
}
@ParameterizedTest
@MethodSource("getSplittableProviders")
void testSplits(SplittableUniformRandomProvider generator) {
assertSplits(generator, false);
}
@ParameterizedTest
@MethodSource("getSplittableProviders")
void testSplitsParallel(SplittableUniformRandomProvider generator) {
assertSplits(generator, true);
}
/**
* Test the splits method returns a stream of unique instances. The test uses a
* fixed source of randomness such that the only randomness is from the stream
* position.
*
* @param generator Generator
* @param parallel true to use a parallel stream
*/
private static void assertSplits(SplittableUniformRandomProvider generator, boolean parallel) {
final long size = 13;
for (final long seed : new long[] {0, RandomStreamsTestHelper.createSeed(ThreadLocalGenerator.INSTANCE)}) {
final SplittableUniformRandomProvider source = new SplittableUniformRandomProvider() {
@Override
public long nextLong() {
return seed;
}
@Override
public SplittableUniformRandomProvider split(UniformRandomProvider source) {
return this;
}
};
// Test the assumption that the seed will be passed through (lowest bit is set)
Assertions.assertEquals(seed | 1, RandomStreamsTestHelper.createSeed(source));
Stream<SplittableUniformRandomProvider> stream = generator.splits(size, source);
Assertions.assertFalse(stream.isParallel(), "Initial stream should be sequential");
if (parallel) {
stream = stream.parallel();
Assertions.assertTrue(stream.isParallel(), "Stream should be parallel");
}
// Check the instance is a new object of the same type.
// These will be hashed using the system identity hash code.
final Set<SplittableUniformRandomProvider> observed = ConcurrentHashMap.newKeySet();
observed.add(generator);
stream.forEach(r -> {
Assertions.assertTrue(observed.add(r), "Instance should be unique");
Assertions.assertEquals(generator.getClass(), r.getClass());
});
// Note: observed contains the original generator so subtract 1
Assertions.assertEquals(size, observed.size() - 1);
// Test instances generate different values.
// The only randomness is from the stream position.
final long[] values = observed.stream().mapToLong(r -> {
// Warm up generator with some cycles.
// E.g. LXM generators return the first value from the initial state.
for (int i = 0; i < 10; i++) {
r.nextLong();
}
return r.nextLong();
}).distinct().toArray();
// This test is looking for different values.
// To avoid the rare case of not all distinct we relax the threshold to
// half the generators. This will spot errors where all generators are
// the same.
Assertions.assertTrue(values.length > size / 2,
() -> "splits did not seed randomness from the stream position. Initial seed = " + seed);
}
}
// Test adapted from stream tests in commons-rng-client-api module
/**
* Helper method to raise an assertion error inside an action passed to a Spliterator
* when the action should not be invoked.
*
* @see Spliterator#tryAdvance(Consumer)
* @see Spliterator#forEachRemaining(Consumer)
*/
private static void failSpliteratorShouldBeEmpty() {
Assertions.fail("Spliterator should not have any remaining elements");
}
@ParameterizedTest
@MethodSource("getSplittableProviders")
void testSplitsInvalidStreamSizeThrows(SplittableUniformRandomProvider rng) {
Assertions.assertThrows(IllegalArgumentException.class, () -> rng.splits(-1), "splits(size)");
final SplittableUniformRandomProvider source = DummyGenerator.INSTANCE;
Assertions.assertThrows(IllegalArgumentException.class, () -> rng.splits(-1, source), "splits(size, source)");
}
@ParameterizedTest
@MethodSource("getSplittableProviders")
void testSplitsUnlimitedStreamSize(SplittableUniformRandomProvider rng) {
assertUnlimitedSpliterator(rng.splits().spliterator(), "splits()");
final SplittableUniformRandomProvider source = ThreadLocalGenerator.INSTANCE;
assertUnlimitedSpliterator(rng.splits(source).spliterator(), "splits(source)");
}
/**
* Assert the spliterator has an unlimited expected size and the characteristics for a sized
* immutable stream.
*
* @param spliterator Spliterator.
* @param msg Error message.
*/
private static void assertUnlimitedSpliterator(Spliterator<?> spliterator, String msg) {
Assertions.assertEquals(Long.MAX_VALUE, spliterator.estimateSize(), msg);
Assertions.assertTrue(spliterator.hasCharacteristics(SPLITERATOR_CHARACTERISTICS),
() -> String.format("%s: characteristics = %s, expected %s", msg,
Integer.toBinaryString(spliterator.characteristics()),
Integer.toBinaryString(SPLITERATOR_CHARACTERISTICS)
));
}
@ParameterizedTest
@MethodSource("getSplittableProviders")
void testSplitsNullSourceThrows(SplittableUniformRandomProvider rng) {
final SplittableUniformRandomProvider source = null;
Assertions.assertThrows(NullPointerException.class, () -> rng.splits(source));
Assertions.assertThrows(NullPointerException.class, () -> rng.splits(1, source));
}
@ParameterizedTest
@MethodSource("getSplittableProviders")
void testSplitsSpliterator(SplittableUniformRandomProvider rng) {
// Split a large spliterator into four smaller ones;
// each is used to test different functionality
final long size = 41;
Spliterator<SplittableUniformRandomProvider> s1 = rng.splits(size).spliterator();
Assertions.assertEquals(size, s1.estimateSize());
final Spliterator<SplittableUniformRandomProvider> s2 = s1.trySplit();
final Spliterator<SplittableUniformRandomProvider> s3 = s1.trySplit();
final Spliterator<SplittableUniformRandomProvider> s4 = s2.trySplit();
Assertions.assertEquals(size, s1.estimateSize() + s2.estimateSize() + s3.estimateSize() + s4.estimateSize());
// s1. Test cannot split indefinitely
while (s1.estimateSize() > 1) {
final long currentSize = s1.estimateSize();
final Spliterator<SplittableUniformRandomProvider> other = s1.trySplit();
Assertions.assertEquals(currentSize, s1.estimateSize() + other.estimateSize());
s1 = other;
}
Assertions.assertNull(s1.trySplit(), "Cannot split when size <= 1");
// Check the instance is a new object of the same type.
// These will be hashed using the system identity hash code.
final HashSet<SplittableUniformRandomProvider> observed = new HashSet<>();
observed.add(rng);
final Consumer<SplittableUniformRandomProvider> action = r -> {
Assertions.assertTrue(observed.add(r), "Instance should be unique");
Assertions.assertEquals(rng.getClass(), r.getClass());
};
// s2. Test advance
for (long newSize = s2.estimateSize(); newSize-- > 0;) {
Assertions.assertTrue(s2.tryAdvance(action));
Assertions.assertEquals(newSize, s2.estimateSize(), "s2 size estimate");
}
Assertions.assertFalse(s2.tryAdvance(r -> failSpliteratorShouldBeEmpty()));
s2.forEachRemaining(r -> failSpliteratorShouldBeEmpty());
// s3. Test forEachRemaining
s3.forEachRemaining(action);
Assertions.assertEquals(0, s3.estimateSize());
s3.forEachRemaining(r -> failSpliteratorShouldBeEmpty());
// s4. Test tryAdvance and forEachRemaining when the action throws an exception
final IllegalStateException ex = new IllegalStateException();
final Consumer<SplittableUniformRandomProvider> badAction = r -> {
throw ex;
};
final long currentSize = s4.estimateSize();
Assertions.assertTrue(currentSize > 1, "Spliterator requires more elements to test advance");
Assertions.assertSame(ex, Assertions.assertThrows(IllegalStateException.class, () -> s4.tryAdvance(badAction)));
Assertions.assertEquals(currentSize - 1, s4.estimateSize(), "Spliterator should be advanced even when action throws");
Assertions.assertSame(ex, Assertions.assertThrows(IllegalStateException.class, () -> s4.forEachRemaining(badAction)));
Assertions.assertEquals(0, s4.estimateSize(), "Spliterator should be finished even when action throws");
s4.forEachRemaining(r -> failSpliteratorShouldBeEmpty());
}
}